文章目录
不同于传统的“一个进程处理一个客户端请求”的方式,IO复用可以让一个进程处理多个客户端的请求,更加节省资源。
前置知识
- 了解socket编程
- 了解五种IO模型
- (红黑树)
为什么需要IO复用
一个简单地服务端可能是这样的:1
2
3
4
5
6
7
8调用socket()创建套接字
bind()绑定地址和端口
listen()监听套接字
while(1){
调用accept()连接客户端
fork()创建进程B来处理客户端的需求/使用新的线程来执行任务
}
释放资源
当使用上面这种方式来处理客户端的请求时,如果客户端数量特别多,服务端就会创建很多进程或线程来执行任务。这种一个进程/线程对应一个客户端的方式其实是挺浪费资源的,如果让一个进程或线程就能够处理多个客户端的连接,那么就能够减少很多不必要的资源浪费。IO就可以解决这个问题。
三种IO复用方法
select
函数API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23int select (int maxfdp, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数:
maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,因为文件描述符是从0开始的
readfds检测可读的文件描述符集合
writefds检测可写的问价描述符集合
exceptfds检测异常条件出现的文件描述符
timeout超时时间
阻塞:设置NULL,会一直阻塞,直到有描述符准备好IO
立即返回:必须设置timeval结构体,但其中的值为0
等待一段时间:在规定时间内如果发生IO活动就马上返回,如果一直没有就等超时后再返回。
返回值:
返回发生所检测操作的fd总数,错误时返回SOCKET_ERROR。
发生io活动的fd存储在相应的参数中(会删除所有传入的fd,只留下发生io活动的)
fd_set
为long类型数组,存储文件描述符。可以用下面几个宏来设置。
FD_ZERO(fd_set *fdset) 将指定的文件描述符集清空
FD_SET(fd_set *fdset) 用于在文件描述符集合中增加一个新的文件描述符。
FD_CLR(fd_set *fdset) 用于在文件描述符集合中删除一个文件描述符。
FD_ISSET(int fd,fd_set *fdset) 用于测试指定的文件描述符是否在该集合中。
struct timeval{
long tv_sec; //seconds
long tv_usec; //microseconds,时间单位比其他的要更小
};使用示例:
poll
- poll原理跟select基本是一样的,但是fd数量没有了限制。
函数API
1
2
3
4
5
6
7
8
9
10
11struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
参数:
pollfd数组传入要检测的IO活动,和返回发生的IO活动
nfds表示文件描述符的最大值加1.
timeout表示超时的毫秒数,负数表示无限阻塞,0表示马上返回。使用示例:
epoll
函数API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_create(int size);
参数:
size以前是用来作fd数目参考,linux2.6.8之后已经不用了
返回值:
如果成功返回一个非负的文件描述符,失败返回-1.
//epoll描述符的控制接口
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数:
epfd,epoll实例的文件描述符。
op是请求的操作:
EPOLL_CTL_ADD
EPOLL_CTL_MOD
EPOLL_CTL_DEL
fd是op操作对应的文件描述符
event标识要检测的io操作,event中的events按位存储发生的事件信息:
EPOLLIN:监测读操作。
EPOLLOUT:写操作。
EPOLLRDHUP:流socket对端关闭连接或关闭写连接。
EPOLLPRI:紧急数据可读
EPOLLERR:关联的文件描述符发生错误
EPOLLHUP:发生挂断。
EPOLLET:设置该fd边缘触发模式
EPOLLONESHOT:用来保证同一SOCKET只能被一个线程处理,不会跨越多个线程。
返回值:
成功返回0,错误返回-1并设置errno。
//来获取发生的IO事件
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
参数:
epfd,epoll实例的文件描述符。
events,返回的io操作
maxevents,要监控的最大文件描述符
timeout表示超时的毫秒数,负数表示无限阻塞,0表示马上返回。
返回值:
返回发生IO事件的fd个数,没有发生返回0,发生错误返回-1.epoll的两种工作模式
水平触发(LT,Level Trigger):默认的模式,如果发生的IO操作没有被处理,下次仍然会继续提醒。并且同时支持 Blocking 和 No-Blocking。
边缘触发(ET,Edge Trigger):高速模式,发生的IO操作只提醒一次,如果没有被处理,下次就不再提醒了。效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。- 使用示例:
三者区别
select
- 单进程支持最大连接数FD_SETSIZE个,一般32位机器位1024,64位为2048。可以重新编译内核修改数量,但性能无法保证。
- bitmap存储fd。
- 将消息从内核空间拷贝到用户空间。
- 每次调用后都需要对所有的fd(一个描述符对应一个客户端连接)进行遍历,随着fd数量增加,性能下降。
- 各个平台都有实现,跨平台效果好。
- 超时精度为纳秒,连接数量少时,实时性较好,适用核反应堆、金融平台等场景。
poll
- 无数量限制,与select本质上没有区别,用链表存储fd。但连接数较多时无法保证性能。
- 链表存储fd
- 将消息从内核空间拷贝到用户空间。(同select)
- 每次调用后都需要对所有的fd(一个描述符对应一个客户端连接)进行遍历,随着fd数量增加,性能下降。(同select)
- 只有新一点的系统支持。
- 超时精度为毫秒。
epoll
- 连接数很大,1G内存机器可以10万左右连接,2G内存可以20万连接。
- 传入fd用红黑树存储,发生io操作的fd用双向链表存储。
- 使用mmap来与内核空间共享内存。
- 不会由于连接数量增加导致性能过分下降,只有首次调用epoll_ctl拷贝fd,每次调用epoll_wait不拷贝。(由于采用回调函数实现。只有活跃的客户端才会调用回调函数,所以epoll会因为活跃的连接数过多而性能下降)
- Linux平台专用。
- 超时精度为毫秒。
三者适用场景
简单地说来,select和poll适合连接数量小、活跃数量多、实时性要求高的情况。而epoll适合客户端的连接数量很大,活跃数量小的情况。